/*******************************************************************************
this code is protected by the GNU affero GPLv3
author:Sylvain BERTRAND <sylvain.bertrand AT gmail dot com>
*******************************************************************************/
#include <libkmod.h>

#include <ulinux/compiler_types.h>
#include <ulinux/types.h>
#include <ulinux/sysc.h>

#include <ulinux/mmap.h>
#include <ulinux/error.h>
#include <ulinux/file.h>
#include <ulinux/fs.h>
#include <ulinux/dirent.h>
#include <ulinux/utils/mem.h>
#include <ulinux/utils/ascii/string/string.h>

#include "out.h"
#include "ulinux_namespace.h"
#include "static_modules.h"

static struct kmod_ctx *kmod_ctx;

static u8 is_current(u8 *n)
{
  if(n[0]=='.'&&n[1]==0) return 1;
  return 0;
}

static u8 is_parent(u8 *n)
{
  if(n[0]=='.'&&n[1]=='.'&&n[2]==0) return 1;
  return 0;
}

/*that for some kmod stuff*/
#define NULL 0
static void driver_modules_probe(u8 *modalias)
{
  i r;
  i log_prio;
  struct kmod_list *list;
  struct kmod_list *list_entry;

  list=0;
  /*shutdown libkmod output since we expect a lot of error/warning messages*/
  log_prio=kmod_get_log_priority(kmod_ctx);
  kmod_set_log_priority(kmod_ctx,0);
  r=kmod_module_new_from_lookup(kmod_ctx,(const char*)modalias,&list);
  kmod_set_log_priority(kmod_ctx,log_prio);
  if(r<0) return;/*we may not have any alias for this hardware, skipping*/

  log_prio=kmod_get_log_priority(kmod_ctx);
  /*same than above*/
  kmod_set_log_priority(kmod_ctx,0);
  kmod_list_foreach(list_entry,list){
    struct kmod_module *m;

    m=kmod_module_get_module(list_entry);
    /*try to probe the driver module, may not be there, keep going in any case*/
    kmod_module_probe_insert_module(m,KMOD_PROBE_IGNORE_COMMAND,0,0,0,0);
    kmod_module_unref(m);
  }
  kmod_set_log_priority(kmod_ctx,log_prio);
  kmod_module_unref_list(list);
}

#define MODALIAS_SZ_MAX PAGE_SZ/*sysfs attributes are of page sz*/
static void modalias_process(i parent_fd)
{
  i fd;
  l r;
  u8 modalias[MODALIAS_SZ_MAX];

  loop{
    fd=(i)ul_openat(parent_fd,"modalias",RDONLY|NONBLOCK);
    if(fd!=-EINTR) break;
  }
  if(ISERR(fd)){
    OUT("WARNING(%d):unable to open modalias, skipping\n",fd);
    return;
  }

  /*the size of a page size reported by the sysfs filesystem is not revelant
    of its content size*/

  loop{
    r=read(fd,modalias,MODALIAS_SZ_MAX);
    if(r!=-EINTR&&r!=-EAGAIN) break;
  }
  if(ISERR(r)){
    OUT("ERROR(%ld):unable to read modalias file\n",r);
    goto close_fd;
  }
 
  modalias[r-1]=0;/*replace the modalias \n terminating char with 0*/

  driver_modules_probe(modalias);

close_fd: 
  loop{
    r=close(fd);
    if(r!=-EINTR) break;
  }
}

#define MODALIAS_FOUND   1
#define MODALIAS_MISSING 0
static u8 modalias_search(u8 *dirents,l ds_sz)
{
  l d_u8_idx=0;
  u8 r=MODALIAS_MISSING;

  loop{
    struct dirent64 *d;
    s8 cmp;

    if(d_u8_idx>=ds_sz) break;

    d=(struct dirent64*)(dirents+d_u8_idx);

    cmp=strcmp("modalias",d->name);
    if(cmp==0){
      r=MODALIAS_FOUND;
      break;
    }
  
    d_u8_idx+=d->rec_len;
  }
  return r;
}

/*forward declaration*/
static void sys_devices_parse(i parent_fd);
static void real_subdirs_recurse(i parent_fd,u8 *dirents,l ds_sz)
{
  l d_u8_idx=0;

  loop{
    struct dirent64 *d;

    if(d_u8_idx>=ds_sz) break;

    d=(struct dirent64*)(dirents+d_u8_idx);

    if(d->type==DT_DIR&&!is_current(d->name)&&!is_parent(d->name)){
      i subdir_fd;

      loop{
        subdir_fd=(i)ul_openat(parent_fd,d->name,RDONLY|NONBLOCK);
        if(subdir_fd!=-EINTR) break;
      }
      if(ISERR(subdir_fd))
        OUT("WARNING(%d):unable to open subdir:%s:skipping\n",subdir_fd,
                                                                       d->name);
      else{
        sys_devices_parse(subdir_fd);
        loop{
          l r;

          r=close(subdir_fd);
          if(r!=-EINTR) break;
        }
      }
    }
    d_u8_idx+=d->rec_len;
  }
}

#define DIRENTS_BUF_SZ 8192
/*
The dentry type is supported by sysfs. Top-down parsing, we load the tree-upper
driver modules first.
*/
static void sys_devices_parse(i parent_fd)
{
  u8 dirents[DIRENTS_BUF_SZ];
  l ds_sz;
  u8 have_modalias;

  ds_sz=getdents64(parent_fd,dirents,DIRENTS_BUF_SZ);
  if(ISERR(ds_sz)){
    OUT("ERROR(%ld):getdents error\n",ds_sz);
    exit_group(-1);
  }

  if(!ds_sz) return;/*no dirents*/

  have_modalias=modalias_search(dirents,ds_sz);

  if(have_modalias==MODALIAS_FOUND) modalias_process(parent_fd);

  real_subdirs_recurse(parent_fd,dirents,ds_sz);
}

void modules_probe_name(u8 *name)
{
  i r;
  struct kmod_module *m;

  r=kmod_module_new_from_name(kmod_ctx,(const char*)name,&m); 

  if(r<0){
    OUT("ERROR(%d):unable to create a module description for %s, skipping\n",
                                                                          name);
    return;
  }

  r=kmod_module_probe_insert_module(m,KMOD_PROBE_IGNORE_COMMAND,0,0,0,0);
  if(r!=0){
    OUT("ERROR(%d):unable to probe module %s, skipping\n",r,name);
    goto unref_module;
  }

unref_module:
  kmod_module_unref(m);
}

/*probe upper layer disk modules and user extra modules*/
void modules_probe_static(void)
{
  u8 **m_name;

  m_name=&static_modules[0];
  loop{
    if(*m_name==0) break;

    modules_probe_name(*m_name);

    ++m_name;
  }
}

void modules_setup(void)
{
  kmod_ctx=kmod_new(0,0);
  if(!kmod_ctx){
    OUT("ERROR:unable to init libkmod\n");
    exit_group(-1);
  }
}

void modules_cleanup(void)
{
  kmod_unref(kmod_ctx);
  kmod_ctx=0;
}

void modules_probe_drivers(void)
{
  i sys_devices_fd;

  loop{
    sys_devices_fd=(i)open("/sys/devices",RDONLY|NONBLOCK);
    if(sys_devices_fd!=-EINTR) break;
  }
  if(ISERR(sys_devices_fd)){
    OUT("ERROR(%d):unable to open /sys/devices dir\n",sys_devices_fd);
    exit_group(-1);
  }

  sys_devices_parse(sys_devices_fd);
  close(sys_devices_fd);
}
